# Boa:Frame:Frame1
# -*- coding:utf-8 -*-

import os
import sys
import time
import threading
import datetime
import json
import traceback
import io

import wx
from wx.adv import TaskBarIcon as wxTaskBarIcon
from wx.adv import EVT_TASKBAR_LEFT_DCLICK

from playsound import playsound
from playsound import PlaysoundException

import pyWinhook
import win32con
import win32api
import ctypes
import pyperclip

import config


VERSION = '3.2.2'


wx.NO_3D = 0
HOT_KEYS = ['F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12']

conf = config.getconfig()


def GetMondrianStream():
    data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00qIDATX\x85\xed\xd6;\n\x800\x10E\xd1{\xc5\x8d\xb9r\x97\x16\x0b\xad$\x8a\x82:\x16o\xda\x84pB2\x1f\x81Fa\x8c\x9c\x08\x04Z{\xcf\xa72\xbcv\xfa\xc5\x08 \x80r\x80\xfc\xa2\x0e\x1c\xe4\xba\xfaX\x1d\xd0\xde]S\x07\x02\xd8>\xe1wa-`\x9fQ\xe9\x86\x01\x04\x10\x00\\(Dk\x1b-\x04\xdc\x1d\x07\x14\x98;\x0bS\x7f\x7f\xf9\x13\x04\x10@\xf9X\xbe\x00\xc9 \x14K\xc1<={\x00\x00\x00\x00IEND\xaeB`\x82'
    stream = io.BytesIO(data)
    return stream


def GetMondrianBitmap():
    stream = GetMondrianStream()
    image = wx.ImageFromStream(stream)
    return wx.BitmapFromImage(image)


def GetMondrianIcon():
    icon = wx.EmptyIcon()
    icon.CopyFromBitmap(GetMondrianBitmap())
    return icon


def create(parent):
    """ 由KeymouseKey.py文件中OnInit方法调用
        创建Frame1类
    """

    return Frame1(parent)


def current_ts():
    """返回当前时间的时间戳秒数（1970纪元后经过的浮点秒数）"""

    return int(time.time() * 1000)


# 下面每个组件的ID
# [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119]
[wxID_FRAME1, wxID_FRAME1BTRECORD, wxID_FRAME1BTRUN, wxID_FRAME1BTPAUSE, wxID_FRAME1BUTTON1,
 wxID_FRAME1CHOICE_SCRIPT, wxID_FRAME1CHOICE_START, wxID_FRAME1CHOICE_STOP,
 wxID_FRAME1PANEL1, wxID_FRAME1STATICTEXT1, wxID_FRAME1STATICTEXT2,
 wxID_FRAME1STATICTEXT3, wxID_FRAME1STATICTEXT4, wxID_FRAME1STIMES,
 wxID_FRAME1TEXTCTRL1, wxID_FRAME1TEXTCTRL2, wxID_FRAME1TNUMRD,
 wxID_FRAME1TSTOP, wxID_FRAME1STATICTEXT5, wxID_FRAME1TEXTCTRL3,
 ] = [wx.NewId() for _init_ctrls in range(20)]

# SW: 屏幕宽度 SH: 屏幕高度
SW = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
SH = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)


class Frame1(wx.Frame):
    """继承自wx.Frame类

        其中wx.Frame类的父类顺序为 wx.Frame -> wx.TopLevelWindow -> wx.NonOwnedWindow -> wx.Window -> wx.EvtHandler
        wx.EvtHandler 继承自 wx.Object 和 wx.Trackable
    """

    def _init_ctrls(self, prnt):
        """初始化组件"""

        # generated method, don't edit
        # 大界面
        wx.Frame.__init__(self,
                          id=wxID_FRAME1,
                          name='',
                          parent=prnt,
                          pos=wx.Point(SW / 2 - 183, SH / 2 - 115.5),
                          size=wx.Size(366, 231),
                          style=wx.STAY_ON_TOP | wx.DEFAULT_FRAME_STYLE,
                          title='KeymouseGo v%s' % VERSION)
        # 大界面大小
        self.SetClientSize(wx.Size(361, 235))

        # 添加组件的面板
        self.panel1 = wx.Panel(id=wxID_FRAME1PANEL1,
                               name='panel1',
                               parent=self,
                               pos=wx.Point(0, 0),
                               size=wx.Size(350, 205),
                               style=wx.NO_3D | wx.CAPTION)

        # 录制/结束按钮
        self.btrecord = wx.Button(id=wxID_FRAME1BTRECORD,
                                  label='录制',
                                  name='btrecord',
                                  parent=self.panel1,
                                  pos=wx.Point(213, 12),
                                  size=wx.Size(56, 32),
                                  style=0)
        self.btrecord.Bind(
            wx.EVT_BUTTON, self.OnBtrecordButton, id=wxID_FRAME1BTRECORD)

        # 启动按钮
        self.btrun = wx.Button(id=wxID_FRAME1BTRUN,
                               label='启动',
                               name='btrun',
                               parent=self.panel1,
                               pos=wx.Point(285, 12),
                               size=wx.Size(56, 32),
                               style=0)
        self.btrun.Bind(wx.EVT_BUTTON, self.OnBtrunButton, id=wxID_FRAME1BTRUN)

        # 暂停/继续 功能不适合用按钮的形式来做，所以暂时隐去
        # self.btpause = wx.Button(id=wxID_FRAME1BTPAUSE, label='暂停',
        #       name='btpause', parent=self.panel1, pos=wx.Point(274, 141),
        #       size=wx.Size(56, 32), style=0)
        # self.btpause.Bind(wx.EVT_BUTTON, self.OnBtpauseButton, id=wxID_FRAME1BTPAUSE)

        # 下方的提示文字
        self.tnumrd = wx.StaticText(id=wxID_FRAME1TNUMRD,
                                    label='ready..',
                                    name='tnumrd',
                                    parent=self.panel1,
                                    pos=wx.Point(17, 205),
                                    size=wx.Size(100, 36),
                                    style=0)

        # 初始不展示的test按钮, 将界面拉大可看见
        self.button1 = wx.Button(id=wxID_FRAME1BUTTON1,
                                 label='test',
                                 name='button1',
                                 parent=self.panel1,
                                 pos=wx.Point(128, 296),
                                 size=wx.Size(75, 24),
                                 style=0)
        self.button1.Bind(wx.EVT_BUTTON, self.OnButton1Button,
                          id=wxID_FRAME1BUTTON1)

        # test按钮下面的文字
        self.tstop = wx.StaticText(id=wxID_FRAME1TSTOP,
                                   label='If you want to stop it, Press F12',
                                   name='tstop',
                                   parent=self.panel1,
                                   pos=wx.Point(25, 332),
                                   size=wx.Size(183, 18),
                                   style=0)
        # 不展示
        self.tstop.Show(False)

        # TODO: 将这个组件的位置放在终止热键下(暂定)
        # 终止热键旁的调节组件
        # 作用: 录制的脚本循环次数
        self.stimes = wx.SpinCtrl(id=wxID_FRAME1STIMES, initial=0, max=1000,
                                  min=0, name='stimes', parent=self.panel1, pos=wx.Point(217, 101),
                                  size=wx.Size(45, 18), style=wx.SP_ARROW_KEYS)
        self.stimes.SetValue(int(conf[2][1]))

        # 启动/暂停热键旁边的文字
        self.label_run_times = wx.StaticText(id=wxID_FRAME1STATICTEXT2,
                                             label='执行次数(0为无限循环)',
                                             name='label_run_times', parent=self.panel1, pos=wx.Point(214, 61),
                                             size=wx.Size(136, 26), style=0)

        # textCtrl1, textCtrl2 为隐藏的两个输入框
        self.textCtrl1 = wx.TextCtrl(id=wxID_FRAME1TEXTCTRL1, name='textCtrl1',
                                     parent=self.panel1, pos=wx.Point(24, 296), size=wx.Size(40, 22),
                                     style=0, value='119')

        self.textCtrl2 = wx.TextCtrl(id=wxID_FRAME1TEXTCTRL2, name='textCtrl2',
                                     parent=self.panel1, pos=wx.Point(80, 296), size=wx.Size(36, 22),
                                     style=0, value='123')

        # 第一行的'脚本'文字框
        self.label_script = wx.StaticText(id=wxID_FRAME1STATICTEXT3,
                                          label='脚本', name='label_script', parent=self.panel1,
                                          pos=wx.Point(17, 20), size=wx.Size(40, 32), style=0)

        # 脚本选择下拉框
        self.choice_script = wx.Choice(choices=[], id=wxID_FRAME1CHOICE_SCRIPT,
                                       name='choice_script', parent=self.panel1, pos=wx.Point(90, 15),
                                       size=wx.Size(108, 25), style=0)

        # 第二行的'启动/暂停热键'文字框
        self.label_start_key = wx.StaticText(id=wxID_FRAME1STATICTEXT1,
                                             label='启动/暂停热键', name='label_start_key',
                                             parent=self.panel1, pos=wx.Point(16, 55), size=wx.Size(56, 36),
                                             style=0)

        # 第三行的'终止热键'文字框
        self.label_stop_key = wx.StaticText(id=wxID_FRAME1STATICTEXT4,
                                            label='终止热键', name='label_stop_key',
                                            parent=self.panel1, pos=wx.Point(16, 102), size=wx.Size(56, 32),
                                            style=0)

        # 第二行启动/暂停热键下拉框
        self.choice_start = wx.Choice(choices=[], id=wxID_FRAME1CHOICE_START,
                                      name='choice_start', parent=self.panel1, pos=wx.Point(90, 58),
                                      size=wx.Size(108, 25), style=0)
        self.choice_start.SetLabel('')
        self.choice_start.SetLabelText('')
        self.choice_start.Bind(wx.EVT_CHOICE, self.OnChoice_startChoice,
                               id=wxID_FRAME1CHOICE_START)

        # 第三行终止热键下拉框
        self.choice_stop = wx.Choice(choices=[], id=wxID_FRAME1CHOICE_STOP,
                                     name='choice_stop', parent=self.panel1, pos=wx.Point(90, 98),
                                     size=wx.Size(108, 25), style=0)
        self.choice_stop.Bind(wx.EVT_CHOICE, self.OnChoice_stopChoice,
                              id=wxID_FRAME1CHOICE_STOP)

        # 第四行'鼠标精度'文字框
        self.label_mouse_interval = wx.StaticText(
            label='鼠标精度', name='label_mouse_interval',
            parent=self.panel1, pos=wx.Point(16, 141), size=wx.Size(56, 32),
            style=0)

        # 第四行鼠标精度的调节组件
        self.mouse_move_interval_ms = wx.SpinCtrl(initial=int(conf[3][1]), max=999999,
                                                  min=0, name='mouse_move_interval_ms', parent=self.panel1, pos=wx.Point(90, 141),
                                                  size=wx.Size(68, 18), style=wx.SP_ARROW_KEYS)

        # 第四行鼠标精度调节组件的说明
        self.label_mouse_interval_tips = wx.StaticText(
            label='数值越小鼠标轨迹越精准，为 0 则不记录', name='label_mouse_interval_tips',
            parent=self.panel1, pos=wx.Point(171, 140), size=wx.Size(150, 50),
            style=0)

        # 第五行'执行速度'文字框
        self.label_execute_speed = wx.StaticText(
            label='执行速度(%)', name='label_execute_speed',
            parent=self.panel1, pos=wx.Point(16, 176), size=wx.Size(70, 32),
            style=0)

        # 第五执行速度文字框调节组件
        self.execute_speed = wx.SpinCtrl(initial=int(conf[4][1]), max=500,
                                         min=20, name='execute_speed', parent=self.panel1,
                                         pos=wx.Point(90, 176),
                                         size=wx.Size(68, 18), style=wx.SP_ARROW_KEYS)

        # 第五行执行速度组件的说明
        self.label_execute_speed_tips = wx.StaticText(
            label='范围(20%-500%)', name='label_execute_speed_tips',
            parent=self.panel1, pos=wx.Point(171, 176), size=wx.Size(150, 50),
            style=0)
        # ===== if use SetProcessDpiAwareness, comment below =====
        # self.label_scale = wx.StaticText(id=wxID_FRAME1STATICTEXT5,
        #       label='屏幕缩放', name='staticText5',
        #       parent=self.panel1, pos=wx.Point(16, 141), size=wx.Size(56, 32),
        #       style=0)
        # self.text_scale = wx.TextCtrl(id=wxID_FRAME1TEXTCTRL3, name='textCtrl3',
        #       parent=self.panel1, pos=wx.Point(79, 138), size=wx.Size(108, 22),
        #       style=0, value='100%')
        # =========================================================

    def __init__(self, parent):
        """初始化Frame1类"""

        # 调用上面的方法
        self._init_ctrls(parent)

        # 设置图标
        # wx.Frame是wx.TopLevelWindow的子类
        # wx.TopLevelWindow.SetIcon(): Sets the icon for this window.
        self.SetIcon(GetMondrianIcon())
        # 设置图标? 下面的TaskBarIcon类
        self.taskBarIcon = TaskBarIcon(self)
        # Bind方法位于wx.EvtHandler类下
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self.Bind(wx.EVT_ICONIZE, self.OnIconfiy)

        # 判断是否存在scripts文件夹, 如果没有创建文件夹
        if not os.path.exists('scripts'):
            os.mkdir('scripts')
        # 将scripts文件夹中的脚本名称倒序并赋值给self.scripts
        self.scripts = os.listdir('scripts')[::-1]
        # 将scripts文件夹中的脚本文件过滤出来并赋值给self.scripts: 尾缀为.txt的文件
        self.scripts = list(filter(lambda s: s.endswith('.txt'), self.scripts))
        # 将筛选出来的脚本文件设置在self.choice_script组件下
        self.choice_script.SetItems(self.scripts)
        # 判断是否存在脚本文件, 如果有选取第一个文件默认展示
        if self.scripts:
            self.choice_script.SetSelection(0)

        # 将HOT_KEYS内容设置在self.choice_start组件中
        self.choice_start.SetItems(HOT_KEYS)
        # 默认设置F6为启动热键
        self.choice_start.SetSelection(int(conf[0][1]))

        # 将HOT_KEYS内容设置在self.choice_stop组件中
        self.choice_stop.SetItems(HOT_KEYS)
        # 默认设置F9为停止热键
        self.choice_stop.SetSelection(int(conf[1][1]))

        # 是否正在启动, 录制
        self.running = False
        self.recording = False
        # 录制脚本的脚本序列
        self.record = []
        # 记录秒数
        self.ttt = current_ts()

        # for pause-resume feature
        self.paused = False
        self.pause_event = threading.Event()

        # 初始化HookManager类
        self.hm = pyWinhook.HookManager()

        def on_mouse_event(event):
            """触发鼠标事件之后调用的方法

                return: 为True才会正常调用，如果为False的话，此次事件被拦截
            """

            # print('MessageName:',event.MessageName)  #事件名称
            # print('Message:',event.Message)          #windows消息常量
            # print('Time:',event.Time)                #事件发生的时间戳
            # print('Window:',event.Window)            #窗口句柄
            # print('WindowName:',event.WindowName)    #窗口标题
            # print('Position:',event.Position)        #事件发生时相对于整个屏幕的坐标
            # print('Wheel:',event.Wheel)              #鼠标滚轮
            # print('Injected:',event.Injected)        #判断这个事件是否由程序方式生成，而不是正常的人为触发。
            # print('---')

            # 如果没有录制, 或者现在为启动状态, 则直接返回
            if not self.recording or self.running:
                return True

            # 消息名称字符串
            message = event.MessageName
            # 判断消息是否为滚轮操作, 如果为滚轮操作则判断其是向上还是向下, 并更新message
            # 向上: event.Wheel == 1
            if message == 'mouse wheel':
                message += ' up' if event.Wheel == 1 else ' down'
            # 所有和鼠标事件有关的消息
            all_messages = ('mouse left down', 'mouse left up', 'mouse right down', 'mouse right up', 'mouse move',
                            'mouse middle down', 'mouse middle up', 'mouse wheel up', 'mouse wheel down')
            # 如果消息事件不是在上面的all_messages里面, 则返回
            if message not in all_messages:
                return True

            # 获得当前指针的位置
            pos = win32api.GetCursorPos()

            # 延时录制脚本
            delay = current_ts() - self.ttt

            # 录制鼠标轨迹的精度，数值越小越精准，但同时可能产生大量的冗余
            mouse_move_interval_ms = self.mouse_move_interval_ms.Value or 999999

            # 如果鼠标为移动, 但是, 过的时间小于设置的延时时间, 则返回True
            if message == 'mouse move' and delay < mouse_move_interval_ms:
                return True

            # 当前时间
            self.ttt = current_ts()
            # 如果当前脚本序列中没有脚本, 则将延时设为0, 即开始运行脚本时, 最开始鼠标的位置
            if not self.record:
                delay = 0

            # 打印, 相对脚本开始时的毫秒时间, 消息, 鼠标位置
            x, y = pos
            # 将x, y坐标转换为相对坐标
            nx = x / SW
            ny = y / SH
            npos = (nx, ny)
            print(delay, message, npos)

            # 添加脚本到脚本序列
            # 'EM'为鼠标
            self.record.append([delay, 'EM', message, npos])

            # 获取显示的内容
            text = self.tnumrd.GetLabel()
            # 获取前面的数字, 即当前录制了多少步
            action_count = text.replace(' actions recorded', '')
            # 更改内容
            text = '%d actions recorded' % (int(action_count) + 1)
            # 将text设置为显示的内容
            self.tnumrd.SetLabel(text)
            return True

        def on_keyboard_event(event):
            """触发键盘点击事件之后调用的方法

                return: 为True才会正常调用，如果为False的话，此次事件被拦截
            """

            # print('MessageName:',event.MessageName)          #同上，共同属性不再赘述
            # print('Message:',event.Message)
            # print('Time:',event.Time)
            # print('Window:',event.Window)
            # print('WindowName:',event.WindowName)
            # print('Ascii:', event.Ascii, chr(event.Ascii))   #按键的ASCII码
            # print('Key:', event.Key)                         #按键的名称
            # print('KeyID:', event.KeyID)                     #按键的虚拟键值
            # print('ScanCode:', event.ScanCode)               #按键扫描码
            # print('Extended:', event.Extended)               #判断是否为增强键盘的扩展键
            # print('Injected:', event.Injected)
            # print('Alt', event.Alt)                          #是某同时按下Alt
            # print('Transition', event.Transition)            #判断转换状态
            # print('---')

            # 通过调试, 这两行貌似可以不需要, 目前我所调试的内容, 但不排除其他情况
            # MessageName: key down
            # MessageName: key up
            message = event.MessageName
            message = message.replace(' sys ', ' ')

            # 如果内容为'key up' 或 没有开始录制
            if message == 'key up' and not self.recording:
                # listen for start/stop script
                # 键的名称并小写化
                key_name = event.Key.lower()
                # start_name = 'f6'  # as default
                # stop_name = 'f9'  # as default

                # GetSelection(): Returns the index of the selected item or NOT_FOUND if no item is selected.
                # 即分别记录开始/结束热键的在队列中的位置
                start_index = self.choice_start.GetSelection()
                stop_index = self.choice_stop.GetSelection()
                # Predict potential conflict: 预测潜在冲突
                # 即判断开始/结束热键的位置是否设置的是一样的, 如果一样则将结束热键的键设置为当前选择热键在序列中的后一位
                if start_index == stop_index:
                    stop_index = (stop_index + 1) % len(HOT_KEYS)
                    self.choice_stop.SetSelection(stop_index)
                # 获取热键的小写名称
                start_name = HOT_KEYS[start_index].lower()
                stop_name = HOT_KEYS[stop_index].lower()

                # 开始录制脚本
                if key_name == start_name and not self.running:
                    print('script start')
                    # 开启录制线程
                    t = RunScriptClass(self, self.pause_event)
                    t.start()
                    print(key_name, 'host start')
                elif key_name == start_name and self.running:
                    # 重新录制脚本

                    # 判断录制是否暂停
                    if self.paused:
                        print('script resume')
                        self.paused = False
                        # 所有条用wait方法的线程将被唤醒
                        self.pause_event.set()
                        print(key_name, 'host resume')
                    else:
                        print('script pause')
                        self.paused = True
                        # 所有条用wait方法的线程将被阻塞
                        self.pause_event.clear()
                        print(key_name, 'host pause')
                elif key_name == stop_name and self.running:
                    # 终止录制脚本
                    print('script stop')
                    self.tnumrd.SetLabel('broken')
                    print(key_name, 'host stop')

            # 如果不是在录制或者正在运行, 则不处理
            if not self.recording or self.running:
                return True

            # 如果不是在all_messages中的情况, 则不处理
            all_messages = ('key down', 'key up')
            if message not in all_messages:
                return True

            # 按下键位的一些信息
            key_info = (event.KeyID, event.Key, event.Extended)

            # 当前相对开始的时间
            delay = current_ts() - self.ttt
            self.ttt = current_ts()
            if not self.record:
                delay = 0

            # 打印录制信息
            print(delay, message, key_info)

            # 内容基本和上面鼠标操作的一样, 详细看上面
            self.record.append([delay, 'EK', message, key_info])
            text = self.tnumrd.GetLabel()
            action_count = text.replace(' actions recorded', '')
            text = '%d actions recorded' % (int(action_count) + 1)
            self.tnumrd.SetLabel(text)
            return True

        # 当鼠标/键盘触发事件之后, 就会分别调用相应方法
        self.hm.MouseAll = on_mouse_event
        self.hm.KeyAll = on_keyboard_event
        # 启动监听鼠标/键盘事件
        self.hm.HookMouse()
        self.hm.HookKeyboard()

    def get_script_path(self):
        """获取脚本路径并返回"""

        # 返回选择脚本的位置, 并赋值给i
        i = self.choice_script.GetSelection()
        if i < 0:
            return ''
        # 从self.scripts选择脚本名称, 并赋值给script
        script = self.scripts[i]
        # os.path.join(path1[, path2[, ...]]): 把目录和文件名合成一个路径
        # os.getcwd(): 获取当前文件路径
        path = os.path.join(os.getcwd(), 'scripts', script)
        print(path)
        return path

    def new_script_path(self):
        """给新的脚本命名, 并获取其路径, 并返回路径"""

        # 获取当前时间
        now = datetime.datetime.now()
        # 对当前时间进行格式化, 并赋值给script
        script = '%s.txt' % now.strftime('%m%d_%H%M')
        # 判断当前名称是否在self.scripts中, 在的话将选择的脚本重命名
        if script in self.scripts:
            script = '%s.txt' % now.strftime('%m%d_%H%M%S')
        # 给self.scripts的首位添加脚本名称
        self.scripts.insert(0, script)
        # 将选择脚本组件的内容更新
        self.choice_script.SetItems(self.scripts)
        # 选择self.scripts[0]
        self.choice_script.SetSelection(0)
        return self.get_script_path()

    # event.Skip(): 确保绑定在同一事件上的不同函数都能执行, 执行顺序是从下往上执行
    def OnHide(self, event):
        # 将组件隐藏
        self.Hide()
        event.Skip()

    def OnIconfiy(self, event):
        self.Hide()
        event.Skip()

    # 关闭按键事件处理函数
    def OnClose(self, event):
        config.saveconfig(newStartIndex=self.choice_start.GetSelection(),
                          newStopIndex=self.choice_stop.GetSelection(),
                          newTimes=self.stimes.GetValue(),
                          newPrecsion=self.mouse_move_interval_ms.GetValue(),
                          newSpeed=self.execute_speed.GetValue())
        self.taskBarIcon.Destroy()
        self.Destroy()
        event.Skip()

    # test按钮事件处理函数
    def OnButton1Button(self, event):
        event.Skip()

    # 结束录制按钮事件处理函数
    def OnBtrecordButton(self, event):

        if self.recording:
            print('record stop')
            self.recording = False
            self.record = self.record[:-2]
            output = json.dumps(self.record, indent=1)
            output = output.replace('\r\n', '\n').replace('\r', '\n')
            output = output.replace('\n   ', '').replace('\n  ', '')
            output = output.replace('\n ]', ']')
            print(output)
            open(self.new_script_path(), 'w').write(output)
            self.btrecord.SetLabel('录制')
            self.tnumrd.SetLabel('finished')
            self.record = []
        else:
            print('record start')
            self.recording = True
            self.ttt = current_ts()
            status = self.tnumrd.GetLabel()
            if 'running' in status or 'recorded' in status:
                return
            self.btrecord.SetLabel('结束')  # 结束
            self.tnumrd.SetLabel('0 actions recorded')
            self.choice_script.SetSelection(-1)
            self.record = []

        event.Skip()

    # 启动按钮事件处理函数
    def OnBtrunButton(self, event):
        print('script start by btn')
        t = RunScriptClass(self, self.pause_event)
        t.start()
        event.Skip()

    # 暂停按钮事件处理函数
    def OnBtpauseButton(self, event):
        print('script pause button pressed')
        if self.paused:
            print('script is resumed')
            self.pause_event.set()
            self.paused = False
            self.btpause.SetLabel('暂停')
        else:
            print('script is paused')
            self.pause_event.clear()
            self.paused = True
            self.btpause.SetLabel('继续')
        event.Skip()

    def OnChoice_startChoice(self, event):
        event.Skip()

    def OnChoice_stopChoice(self, event):
        event.Skip()


class RunScriptClass(threading.Thread):

    def __init__(self, frame: Frame1, event: threading.Event):
        self.frame = frame
        self.event = event
        self.event.set()
        super(RunScriptClass, self).__init__()

    def run(self):

        # 通过获取面板的提示判断是否在运行
        status = self.frame.tnumrd.GetLabel()
        # 判断frame是否在运行或是否是录制状态, 如果是则不运行下面的代码
        if self.frame.running or self.frame.recording:
            return

        # 判断是不是现在正在运行或是录制, 如果是则不运行下面的代码(保障)
        if 'running' in status or 'recorded' in status:
            return

        # 获取脚本路径
        script_path = self.frame.get_script_path()
        if not script_path:
            self.frame.tnumrd.SetLabel(
                'script not found, please self.record first!')
            return

        # 设置为正在运行
        self.frame.running = True

        try:
            # 获取需要运行的次数
            self.run_times = self.frame.stimes.Value
            # 将运行的脚本名称显示出来
            self.running_text = '%s running..' % script_path.split(
                '/')[-1].split('\\')[-1]
            # 设置在展示面板
            self.frame.tnumrd.SetLabel(self.running_text)
            self.frame.tstop.Shown = True
            # 运行速度
            self.run_speed = self.frame.execute_speed.Value

            # 脚本已经运行了多少次
            self.j = 0
            while self.j < self.run_times or self.run_times == 0:
                self.j += 1
                current_status = self.frame.tnumrd.GetLabel()
                if current_status in ['broken', 'finished']:
                    self.frame.running = False
                    break
                # 运行脚本
                RunScriptClass.run_script_once(script_path, self.j, thd=self)

            # 结束脚本的一些设置, 并将有些内容显示出去
            self.frame.tnumrd.SetLabel('finished')
            self.frame.tstop.Shown = False
            self.frame.running = False
            # 播放脚本结束提示
            PlayPromptTone.play_end_sound()
            print('script run finish!')

        except Exception as e:
            # 异常
            print('run error', e)
            traceback.print_exc()
            self.frame.tnumrd.SetLabel('failed')
            self.frame.tstop.Shown = False
            self.frame.running = False

    @classmethod
    def run_script_once(cls, script_path, step, thd=None):
        """运行一次脚本

            cls:            当前的类名 (@classmethod注解需要添加的参数)
            script_path:    脚本路径
            step:           现在运行的次数
            thd:            线程
        """

        # 脚本内容
        content = ''

        # 选择的脚本
        lines = []

        try:
            # 获取编码格式为utf8格式的文件脚本
            lines = open(script_path, 'r', encoding='utf8').readlines()
        except Exception as e:
            print(e)
            try:
                # 获取编码格式为gbk格式的文件脚本
                lines = open(script_path, 'r', encoding='gbk').readlines()
            except Exception as e:
                print(e)

        for line in lines:
            # 去注释
            if '//' in line:
                index = line.find('//')
                line = line[:index]
            # 去空字符
            line = line.strip()
            content += line

        # 去最后一个元素的逗号（如有）
        content = content.replace('],\n]', ']\n]').replace('],]', ']]')

        print(content)
        s = json.loads(content)
        steps = len(s)

        # 从0开始
        for i in range(steps):
            # 打印脚本
            print(s[i])

            # 相对脚本开始的延时事件
            delay = s[i][0] / (thd.run_speed/100)
            # 操作类型: 鼠标/键盘/其他
            event_type = s[i][1].upper()
            # 操作动作
            message = s[i][2].lower()
            # 操作动作需要的参数
            action = s[i][3]

            # 延时结束时, 提示音
            # 当现在为第一次运行, 且为第一步时, 进行判断
            if step == 1 and i == 0:
                play = PlayPromptTone(1, delay)
                play.start()

            time.sleep(delay / 1000.0)

            if thd:
                current_status = thd.frame.tnumrd.GetLabel()
                if current_status in ['broken', 'finished']:
                    break
                thd.event.wait()
                text = '%s  [%d/%d %d/%d] %d%%' % (
                    thd.running_text, i+1, steps, thd.j, thd.run_times, thd.run_speed)
                thd.frame.tnumrd.SetLabel(text)

            # 操作
            if event_type == 'EM':
                x, y = action

                if action == [-1, -1]:
                    # 约定 [-1, -1] 表示鼠标保持原位置不动
                    pass
                else:
                    # 挪动鼠标 普通做法
                    # ctypes.windll.user32.SetCursorPos(x, y)
                    # or
                    # win32api.SetCursorPos([x, y])

                    # 更好的兼容 win10 屏幕缩放问题
                    # 比例乘当前屏幕尺寸, 使鼠标在不同屏幕的情况下, 相对位置不变
                    nx = int((x * SW) * 65535 / SW)
                    ny = int((y * SH) * 65535 / SH)
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_ABSOLUTE | win32con.MOUSEEVENTF_MOVE, nx , ny , 0, 0)

                if message == 'mouse left down':
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
                elif message == 'mouse left up':
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
                elif message == 'mouse right down':
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0)
                elif message == 'mouse right up':
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0)
                elif message == 'mouse middle down':
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, 0)
                elif message == 'mouse middle up':
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0)
                elif message == 'mouse wheel up':
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_WHEEL, 0, 0, 120, 0)
                elif message == 'mouse wheel down':
                    win32api.mouse_event(
                        win32con.MOUSEEVENTF_WHEEL, 0, 0, -120, 0)
                elif message == 'mouse move':
                    pass
                else:
                    print('unknow mouse event:', message)

            elif event_type == 'EK':
                key_code, key_name, extended = action

                # shift ctrl alt
                # if key_code >= 160 and key_code <= 165:
                #     key_code = int(key_code/2) - 64

                base = 0
                if extended:
                    base = win32con.KEYEVENTF_EXTENDEDKEY

                if message == 'key down':
                    win32api.keybd_event(key_code, 0, base, 0)
                elif message == 'key up':
                    win32api.keybd_event(
                        key_code, 0, base | win32con.KEYEVENTF_KEYUP, 0)
                else:
                    print('unknow keyboard event:', message)

            elif event_type == 'EX':

                if message == 'input':
                    text = action
                    pyperclip.copy(text)
                    # Ctrl+V
                    win32api.keybd_event(162, 0, 0, 0)  # ctrl
                    win32api.keybd_event(86, 0, 0, 0)  # v
                    win32api.keybd_event(86, 0, win32con.KEYEVENTF_KEYUP, 0)
                    win32api.keybd_event(162, 0, win32con.KEYEVENTF_KEYUP, 0)
                else:
                    print('unknow extra event:', message)


class TaskBarIcon(wxTaskBarIcon):
    ID_About = wx.NewId()
    ID_Closeshow = wx.NewId()

    def __init__(self, frame):
        wxTaskBarIcon.__init__(self)
        self.frame = frame
        self.SetIcon(GetMondrianIcon())
        self.Bind(EVT_TASKBAR_LEFT_DCLICK, self.OnTaskBarLeftDClick)
        self.Bind(wx.EVT_MENU, self.OnAbout, id=self.ID_About)
        self.Bind(wx.EVT_MENU, self.OnCloseshow, id=self.ID_Closeshow)

    def OnTaskBarLeftDClick(self, event):
        if self.frame.IsIconized():
            self.frame.Iconize(False)
        if not self.frame.IsShown():
            self.frame.Show(True)
        self.frame.Raise()

    def OnAbout(self, event):
        wx.MessageBox('https://github.com/taojy123/KeymouseGo',
                      'KeymouseGo v%s' % VERSION)
        event.Skip()

    def OnCloseshow(self, event):
        self.frame.Close(True)
        event.Skip()

    def CreatePopupMenu(self):
        menu = wx.Menu()
        menu.Append(self.ID_About, 'About')
        menu.Append(self.ID_Closeshow, 'Exit')
        return menu

class PlayPromptTone(threading.Thread):

    def __init__(self, op, delay):
        """初始化    
            
            op: 操作类型, 1: 启动
            delay: 延时(毫秒)"""
        self.__delay = delay
        self.__op = op
        super().__init__()

    def run(self):
        # 保证为开始
        if 1 == self.__op:
            # 判断延时是不是超过1s
            if self.__delay >= 1000:
                time.sleep((self.__delay - 500.0) / 1000.0)
            self._play_start_sound()

    def _play_start_sound(self):
        """播放脚本开始提示音"""
        try:
            path = os.path.join(os.getcwd(), 'sounds', 'start.mp3')
            playsound(path)
        except PlaysoundException as e:
            print(e)

    @classmethod
    def play_end_sound(cls):
        """播放脚本结束提示音"""
        try:
            path = os.path.join(os.getcwd(), 'sounds', 'end.mp3')
            playsound(path)
        except PlaysoundException as e:
            print(e)